<?php
// +-------------------------------------------------------------------------+
// | PHP Version 5                                                           |
// +-------------------------------------------------------------------------+
// | ImaEngine - web development framework                                   |
// | Copyright (c) 2006-2007 Sardar Yumatov <ja.doma@gmail.com>              |
// +-------------------------------------------------------------------------+
// | This library is free software; you can redistribute it and/or modify it |
// | under the terms of the GNU Lesser General Public License as published   |
// | by the Free Software Foundation; either version 2.1 of the License, or  |
// | (at your option) any later version.                                     |
// |                                                                         |
// | This library is distributed in the hope that it will be useful, but     |
// | WITHOUT ANY WARRANTY; without even the implied warranty of              |
// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.                    |
// | See the GNU Lesser General Public License for more details.             |
// |                                                                         |
// | You should have received a copy of the GNU Lesser General Public        |
// | License along with this library; if not, write to the Free Software     |
// | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
// +-------------------------------------------------------------------------+

/**
* Templater implementation with translation to PHP.
* Most templates will use this templater since it is faster in most cases, however parsing templater with cache
* may be faster if content doesn't change very often.
*
* @author Sardar Yumatov <ja.doma@gmail.com>
* @copyright Copyright &copy; 2006-2007 Sardar Yumatov
* @version v1.0
* @package common.template
* @category Templater
*
* @PHP5only
*/

if(!defined('PROCESS')) {
	header('HTTP/1.1 404 Not Found');
	die('NotFound');
}

require_once('lib/GenericExceptions.php');
require_once('lib/FileUtils.php');
require_once('lib/template/AbstractTemplateDocument.php');
//included down in the code if compiling is required
//require_once('lib/template/TemplateParser.php');

/**
 * Compiled template loader.
 * Class compiles the source template to a PHP code, then includes it and instantiates the blocks.
 *
 * This class doesn't implement any naming scheme. Use this class if you know the path to the template file,
 * otherwise use a high-level wrapper instead.
 *
 * @author Sardar Yumatov <ja.doma@gmail.com>
 */
class CompiledTemplateDocument extends AbstractTemplateDocument {

	/**
	 * The absolute path to the compiled template file.
	 * @var string
	 */
	private $compiled_path;

	/**
	 * The absolute path to the template file.
	 * @var string
	 */
	private $path;

	/**
	 * Load the template.
	 *
	 * Constructor tries to find compiled tempate class by $name provided with arguments.
	 * If class doen't exist, then $compile_file will be loaded. If $compile_file doesn't
	 * exist or is not updated, then it will be created/updated from template source $file.
	 *
	 * @throws NotFoundException if source template isn't found
	 * @throws SyntaxException on any syntax exception while compiling the source template
	 * @throws IOException on any IO error
	 * @param string $name name of the template, will be used to instantiate compiled stub class
	 * @param string|FilePath $source path to template source file
	 * @param string|FilePath $compile_source path where compiled template (cache) will be saved
	 */
	public function __construct($name, $source, $compile_source) {
		$class = CompiledTemplateDocumentCompiler::getRootblockName($name);
		CompiledTemplateDocumentCompiler::loadTemplateStub($name, $source, $compile_source); //throws NotFoundException and SyntaxException

		$this->rootblock = new $class($this); //throws RuntimeException
		$this->blocks = $this->rootblock->getBlocks();

		parent::__construct($name);
		$this->path = FileUtils::getPath($source);
		$this->compiled_path = FileUtils::getPath($compile_source);
	}
	/**
	 * Returns path to the compiled template file.
	 * @return string
	 */
	public function getCompiledPath() {
		return $this->compiled_path;
	}

	/**
	 * Returns path to the template file.
	 * @return string
	 */
	public function getPath() {
		return $this->path;
	}
}


/**
 * The template compiler.
 *
 * This class uses default grabber (handler) from the template parser to collect the data.
 * Then collected data will be rendered to PHP source.
 *
 * @author Sardar Yumatov <ja.doma@gmail.com>
 */
class CompiledTemplateDocumentCompiler {

	/**
	 * Ensure the stub template for the $name is loaded.
	 * The template will be compiled and loaded on demand.
	 *
	 * @throws NotFoundException if $source template is not found
	 * @throws SyntaxException if source template contains syntax errors
	 * @throws IOException on any IO error
	 * @param string $name name of the template, will be used to instantiate compiled stub class
	 * @param string|FilePath $source path to template source file
	 * @param string|FilePath $compile_source path where compiled template (cache) will be saved
	 * @return void
	 */
	public static function loadTemplateStub($name, $source, $compile_source) {
		$class = self::getRootblockName($name);
		if(class_exists($class)) return;

		$source = FilePath::createPath($source, true);
		$compile_source = FilePath::createPath($compile_source, true);
		$srcHandler = $source->getHandler(); $compHandler = $compile_source->getHandler();
		if(!$srcHandler->exists()) throw new NotFoundException("Tempalte file '{$source->getPath()}' is not found!");

		$err = false;
		do {// we try to be self repair is someone has imported non supported or broken cache files
			if($err || !$compHandler->isFile() || $compHandler->getLastModifedTime() < $srcHandler->getLastModifedTime()) //recompile template
				self::compile($name, $source, $compile_source); //throws SyntaxException, IOException, RuntimeException

			if(false === (include($compile_source->getPath())) || !class_exists($class)) {
				if(!$err) { //try to fix
					trigger_error("Compiled template document '{$compile_source->getPath()}' is damaged! Template: '{$source->getPath()}'", E_USER_WARNING);
					$compile_source->getHandler()->delete();
					clearstatcache();
					$err = true;
					continue;
				} else throw new LogicException("Compiler error, compiled template '{$name}' is either damaged or compiler has a bug. File: '{$source->getPath()}', bin: '{$compile_source->getPath()}'");
			}
			break;
		} while(true);
	}

	/**
	 * Construct the root block name.
	 * Root block doesn't has a name at all, each template has at least one block - the root block.
	 * @param string $template the template name
	 * @return string the name of the compiled root block class
	 */
	public static function getRootblockName($template) { //change if compiled format changes
		if(!strlen($template = trim($template))) throw new InvalidArgumentException("Given root block name is empty!");
		return 'CompiledTemplateDocument_template2_root_'.sha1($template);
	}

	/**
	 * Construct compiled PHP class name of the named block.
	 * @param string $template the template name containing this block
	 * @param string $name the block name
	 * @return string compiled block class name
	 */
	public static function getBlockName($template, $name) { //change if compiled format changes
		if(!strlen($template = trim($template)) || !strlen($name = trim($name))) throw new InvalidArgumentException("Either template name '{$template}' or sub-block name '{$name}' is empty!");
		return 'CompiledTemplateDocument_template2_block_'.sha1($template).'_'.sha1($name);
	}

	/**
	 * Compile given $source template and save to $destination file.
	 *
	 * @throws NotFoundException if $source template is not found
	 * @throws SyntaxException on any syntax error
	 * @throws IOException on any IO error
	 * @param string $name template name
	 * @param string|FilePath $source path to template source file
	 * @param string|FilePath $destination path to compiled template, where compiled template will be saved.
	 * @return void
	 */
	public static function compile($name, $source, $destination) {
		require_once('lib/template/TemplateParser.php'); //lazy include

		$source = FilePath::createPath($source, true);
		$destination = FilePath::createPath($destination, true);
		$hndl = $source->getHandler();
		if(!$hndl->exists()) throw new NotFoundException("Tempalte file '{$source->getPath()}' is not found!");
		$text = $hndl->getFileContents(); //throws IOException

		//parse content
		$grabber = new DefaultTemplateParserHandler();
		TemplateParser::parse($grabber, $text); //throws SyntaxException

		$ret = self::renderRoot($name, $grabber);
		$time = date(DATE_RFC1123);
		$data = <<<PHPEOF
<?php
/**
* THIS FILE IS GENERATED BY MACHINE! DO NOT EDIT!
*
* TemplateDocument name: {$name}
* Generated at: {$time}
* From source file: {$source->getPath()}
* Should be written to: {$destination->getPath()}
*
* @author Sardar Yumatov <ja.doma@gmail.com>
*/

{$ret}

?>
PHPEOF;
		$destination->getHandler()->putFileContents($data); //throws IOException
	}

	/**
	 * Render root (stub) block.
	 * @param string $template name of the template
	 * @param DefaultTemplateParserHandler $root the handler collects the
	 * @return string
	 */
	private static function renderRoot($template, DefaultTemplateParserHandler $root) {
		$class = self::getRootblockName($template);
		$innerblocks = '';
		$childs = '';
		foreach($root->getBlocks() as $block) {
			$chname = var_export($block->getName(), true);
			$innerblocks .= "{$chname} => new ".self::getBlockName($template, $block->getName())."(\$template, \$this),\n";
			$childs .= self::renderBlock($template, $block);
		}
		$body = '';
		foreach ($root->getRootBlock()->getBody() as $entry) $body.=is_string($entry)? self::ensureSafeContent($entry): self::generateTagCall($entry);
		return <<<PHPEOF
/**
* Root block.
* @author Sardar Yumatov <ja.doma@gmail.com>
*/
class {$class} extends AbstractTemplateDocumentBlock {

	//list of all child blocks
	private \$blocks;

	/**
	 * Create new root block.
	 * @param CompiledTemplateDocument \$template the template document instance, that is loading this block
	 */
	public function __construct(CompiledTemplateDocument \$template) {
		parent::__construct(null, null, \$template);
		//root block has never pre-filter chain
		\$this->pre_filter_chain = null;
		//child blocks
		\$this->blocks = array(
{$innerblocks}
		);
	}

	/** Returns all blocks of this template */
	public function getBlocks() {
		return \$this->blocks;
	}

	/**
	 * Generate the text output of this block.
	 * All found tags should be send to the \$handler.
	 * @param TemplateDocumentHandler \$handler template handler
	 * @return void the text content should be send to output buffer
	 */
	protected function generateOutput(TemplateDocumentHandler \$handler) {
?>
{$body}
<?php
	}
}

{$childs}

PHPEOF;
	}

	/**
	 * Render a block as PHP class
	 * @param string $template name of the template
	 * @param DefaultTemplateParserHandlerBlock $block the block to render
	 * @return string class source text
	 */
	private static function renderBlock($template, $block) {
		$class = self::getBlockName($template, $block->getName());
		$safename = var_export($block->getName(), true);
		$filters = $block->getFilters();
		$filters = !empty($filters)? var_export($filters, true): 'null';
		$body = '';
		foreach ($block->getBody() as $entry) $body.=is_string($entry)? self::ensureSafeContent($entry): self::generateTagCall($entry);
		return <<<PHPEOF
/**
* Block name: {$block->getName()}
* @author Sardar Yumatov <ja.doma@gmail.com>
*/
class {$class} extends AbstractTemplateDocumentBlock {

	/**
	 * Create new template instance
	 * @param CompiledTemplateDocument \$template the template document instance, that is loading this block
	 * @param AbstractTemplateDocumentBlock \$parent the parent block
	 */
	public function __construct(CompiledTemplateDocument \$template, AbstractTemplateDocumentBlock \$parent) {
		parent::__construct({$safename}, \$parent, \$template);
		//init filters
		\$this->pre_filter_chain = {$filters};
	}

	/**
	 * Generate the text output of this block.
	 * All found tags should be send to the \$handler.
	 * @param TemplateDocumentHandler \$handler template handler
	 * @return void the text content should be send to output buffer
	 */
	protected function generateOutput(TemplateDocumentHandler \$handler) {
?>
{$body}
<?php
	}
}

PHPEOF;
	}

	/**
	 * Generated PHP code for tag execution call.
	 * @param array $entry the tag as collected by default parser handler
	 * @return string generated code
	 */
	private static function generateTagCall($entry) {
		$action = var_export($entry['action'], true);
		$markers = var_export($entry['markers'], true);

		$data = "<?php
ob_start();
if(\$handler->processTag(\$this, {$action}, {$markers})) {\n";

		if(empty($entry['filters'])) $data.="ob_end_flush();\n";
		else { //generate filter calls
			$filters = var_export($entry['filters'], true);
			$data.="\$cont = ob_get_clean();
//process filters
\$filters = {$filters};
foreach(\$filters as \$name=>\$params)
	\$cont = AbstractTemplateDocument::executeFilter(\$name, \$cont, \$params);
echo \$cont;\n";
		}

		$data.="} else ob_end_clean(); //discard output
?>\n";
		return $data;
	}

	/**
	 * Ensures the content is safe text, thus no <?php sequences are allowed.
	 * @param string $txt text to process
	 * @return string safe text
	 */
	public static function ensureSafeContent($txt) {
		//str_replace is uqly, will replace ? > of our echo again, PHP sucks =(
		/* return str_replace(array('<?', '?>'), array('<?php echo \'<?\'; ?>', '<?php echo \'?>\'; ?>'), $txt); */
		return preg_replace('/((?:\<\?)|(?:\?\>))/', "<?php echo '\\1'; ?>", $txt);
	}
}
?>